Skip to content

Automated Test: oauth-state-secure #330

Closed
wants to merge 1 commit into from

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added OAuth-based authentication flow for GitHub integration setup, enabling secure authorization and verification of GitHub user accounts.
    • Enhanced error handling and validation during installation with clearer, more descriptive error messages for better user guidance.
  • Improvements

    • Improved installation workflow with better state management to gracefully handle edge cases and prevent installation conflicts.

…#67876)

We're adding one more step in the GitHub integration installation
pipeline, namely GitHub OAuth2 authorize. This is transparent from the
UX perspective as the data exchange happens without user interaction.

The pipeline will now fail in these cases:
- If there is a mismatch between currently authenticated GitHub user
(derived from OAuth2 authorize step) and the user who installed the
GitHub app (https://github.com/apps/sentry-io)
- If there is a mismatch between `state` parameter supplied by user and
pipeline signature
- If GitHub could not generate correct `access_token` from the `code`
(wrong or attempt of re-use of `code`).

In all those cases, this error is shown:

![image](https://github.com/getsentry/sentry/assets/1127549/18923861-2ead-4cf5-adda-7738aef801d7)
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

These changes implement an OAuth-based login flow for GitHub integration setup, introducing an OAuthLoginView class that handles GitHub authorization before installation. The implementation includes error handling utilities, updates to pipeline views to include the new OAuth step, removal of a multi-provider constant in the pipeline advancer, and corresponding test coverage.

Changes

Cohort / File(s) Summary
GitHub OAuth and Error Handling
src/sentry/integrations/github/integration.py
Introduces OAuthLoginView class implementing GitHub OAuth authorization flow; adds error() and get\_document\_origin() helper functions for error rendering; adds ERR\_INTEGRATION\_INVALID\_INSTALLATION\_REQUEST constant; updates GitHubIntegration and GitHubIntegrationProvider pipeline views to include OAuthLoginView as first step; modifies installation\_id retrieval to source from pipeline state when absent from request.
Pipeline Advancer Configuration
src/sentry/web/frontend/pipeline_advancer.py
Removes FORWARD\_INSTALL\_FOR constant and replaces multi-provider conditional logic with direct provider\_id == "github" check for install setup actions.
GitHub Integration Test Coverage
tests/sentry/integrations/github/test_integration.py
Adds OAuth token and user endpoint mocks; introduces test coverage for GitHub user mismatch and existing installations on different organizations; validates OAuth redirect URL parameters (client\_id, state, redirect\_uri); adjusts test flow with setup\_path\_2 for repeated installation attempts and conflict scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant OAuthLoginView
    participant GitHub
    participant Pipeline
    participant GitHubInstallation

    Client->>OAuthLoginView: initiate setup
    OAuthLoginView->>OAuthLoginView: generate state & redirect_uri
    OAuthLoginView-->>Client: redirect to GitHub authorize URL
    Client->>GitHub: authorize with client_id & state
    GitHub-->>Client: redirect with authorization code
    Client->>OAuthLoginView: callback with code & state
    OAuthLoginView->>OAuthLoginView: validate state
    OAuthLoginView->>GitHub: exchange code for access_token
    GitHub-->>OAuthLoginView: return access_token
    OAuthLoginView->>GitHub: fetch user info
    GitHub-->>OAuthLoginView: return user info
    OAuthLoginView->>Pipeline: store authenticated username
    OAuthLoginView-->>Pipeline: advance to next step
    Pipeline->>GitHubInstallation: process installation
    GitHubInstallation-->>Client: installation complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through OAuth's gate,
GitHub's flow now runs first-rate,
Errors handled with finesse so fine,
State and tokens align just right,
Installation blooms in pipeline light! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title 'Automated Test: oauth-state-secure' is vague and generic. While it references OAuth and state security, it does not clearly describe the substantial changes made (OAuth login flow, error handling utilities, pipeline updates, and expanded test coverage). Consider a more descriptive title that captures the main changes, such as 'Add OAuth login flow and error handling for GitHub integration' or similar that reflects the actual implementation scope.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch oauth-state-secure

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/sentry/integrations/github/test_integration.py (1)

378-396: ⚠️ Potential issue | 🟠 Major

test_installation_not_found is testing state validation failure, not an installation lookup failure.

The test sets up a 404 mock on the /app/installations/{id} endpoint (line 381-383), but this code path is never reached. The hardcoded state ddd023d87a913d5226e2a882c4c4cc05 (line 392) does not match the pipeline's signature 9cae5e88803f35ed7970fc131e6e65d3 (established in setUp() and verified in assert_setup_flow()). This causes OAuthLoginView.dispatch() to fail the state validation check on line 412 of integration.py and return error() with the default message "Invalid installation request." before attempting the installation lookup.

The test assertion passes, but it's validating the wrong scenario. To properly test the 404 case, the state parameter must match the pipeline signature (as shown in assert_setup_flow() on line 242).

🤖 Fix all issues with AI agents
In `@src/sentry/integrations/github/integration.py`:
- Around line 425-436: The call to get_user_info(payload["access_token"]) can
raise network or API errors but isn't wrapped, causing 500s; wrap the
get_user_info invocation (and any subsequent access to authenticated_user_info)
in a try/except, return the friendly error(request, self.active_organization) on
failure, and log the exception (include the token context minimally) so failures
in get_user_info are handled gracefully; update the block that currently decodes
body/safely parses payload and the later "if 'login' not in
authenticated_user_info" check to use the protected result.
- Around line 493-505: The code assumes integration.metadata["sender"]["login"]
always exists and can raise KeyError; update the check around
pipeline.fetch_state("github_authenticated_user") to safely access
integration.metadata by verifying "sender" exists and has a "login" (e.g., check
"sender" in integration.metadata and "login" in integration.metadata["sender"])
or use a helper/get with default None, then only compare when present; reference
the Integration lookup (Integration.objects.get), the integration.metadata
access, and pipeline.fetch_state("github_authenticated_user") and fall back to
returning error(request, self.active_organization) if sender/login is missing or
does not match.
🧹 Nitpick comments (1)
src/sentry/integrations/github/integration.py (1)

404-408: Query parameters in the redirect URL are not URL-encoded.

The redirect_uri value contains characters like : and / that should be percent-encoded when used as a query parameter value. While GitHub's OAuth endpoint likely handles this gracefully, it's not standards-compliant and could break with stricter URL parsers.

Consider using urlencode (already imported via parse_qsl's module) to construct the query string:

♻️ Proposed fix
+from urllib.parse import parse_qsl, urlencode
...
-            return self.redirect(
-                f"{ghip.get_oauth_authorize_url()}?client_id={github_client_id}&state={state}&redirect_uri={redirect_uri}"
-            )
+            params = urlencode({
+                "client_id": github_client_id,
+                "state": state,
+                "redirect_uri": redirect_uri,
+            })
+            return self.redirect(
+                f"{ghip.get_oauth_authorize_url()}?{params}"
+            )

Comment on lines +425 to +436
try:
body = safe_urlread(req).decode("utf-8")
payload = dict(parse_qsl(body))
except Exception:
payload = {}

if "access_token" not in payload:
return error(request, self.active_organization)

authenticated_user_info = get_user_info(payload["access_token"])
if "login" not in authenticated_user_info:
return error(request, self.active_organization)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing error handling for get_user_info — unhandled exceptions will 500.

The token exchange on lines 425-429 is wrapped in a try/except, but the get_user_info call on line 434 is unprotected. If the GitHub /user API call fails (network error, rate limit, invalid token), this will raise an unhandled exception and return a 500 to the user instead of the friendly error page.

🐛 Proposed fix
-        authenticated_user_info = get_user_info(payload["access_token"])
-        if "login" not in authenticated_user_info:
+        try:
+            authenticated_user_info = get_user_info(payload["access_token"])
+        except Exception:
+            return error(request, self.active_organization)
+
+        if "login" not in authenticated_user_info:
             return error(request, self.active_organization)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
body = safe_urlread(req).decode("utf-8")
payload = dict(parse_qsl(body))
except Exception:
payload = {}
if "access_token" not in payload:
return error(request, self.active_organization)
authenticated_user_info = get_user_info(payload["access_token"])
if "login" not in authenticated_user_info:
return error(request, self.active_organization)
try:
body = safe_urlread(req).decode("utf-8")
payload = dict(parse_qsl(body))
except Exception:
payload = {}
if "access_token" not in payload:
return error(request, self.active_organization)
try:
authenticated_user_info = get_user_info(payload["access_token"])
except Exception:
return error(request, self.active_organization)
if "login" not in authenticated_user_info:
return error(request, self.active_organization)
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 428-428: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@src/sentry/integrations/github/integration.py` around lines 425 - 436, The
call to get_user_info(payload["access_token"]) can raise network or API errors
but isn't wrapped, causing 500s; wrap the get_user_info invocation (and any
subsequent access to authenticated_user_info) in a try/except, return the
friendly error(request, self.active_organization) on failure, and log the
exception (include the token context minimally) so failures in get_user_info are
handled gracefully; update the block that currently decodes body/safely parses
payload and the later "if 'login' not in authenticated_user_info" check to use
the protected result.

Comment on lines +493 to +505
try:
integration = Integration.objects.get(
external_id=installation_id, status=ObjectStatus.ACTIVE
)
except Integration.DoesNotExist:
return error(request, self.active_organization)

# Check that the authenticated GitHub user is the same as who installed the app.
if (
pipeline.fetch_state("github_authenticated_user")
!= integration.metadata["sender"]["login"]
):
return error(request, self.active_organization)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential KeyError if integration metadata lacks a "sender" key.

Line 503 accesses integration.metadata["sender"]["login"] without checking whether the "sender" key exists. The build_integration method (line 376) only sets metadata["sender"] conditionally when state.get("sender") is present. For integrations created without a sender (e.g., older installations or webhook-created ones), this will raise a KeyError and return a 500 error.

🐛 Proposed fix
         try:
             integration = Integration.objects.get(
                 external_id=installation_id, status=ObjectStatus.ACTIVE
             )
         except Integration.DoesNotExist:
             return error(request, self.active_organization)
 
+        sender_login = (integration.metadata or {}).get("sender", {}).get("login")
         # Check that the authenticated GitHub user is the same as who installed the app.
         if (
             pipeline.fetch_state("github_authenticated_user")
-            != integration.metadata["sender"]["login"]
+            != sender_login
         ):
             return error(request, self.active_organization)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
integration = Integration.objects.get(
external_id=installation_id, status=ObjectStatus.ACTIVE
)
except Integration.DoesNotExist:
return error(request, self.active_organization)
# Check that the authenticated GitHub user is the same as who installed the app.
if (
pipeline.fetch_state("github_authenticated_user")
!= integration.metadata["sender"]["login"]
):
return error(request, self.active_organization)
try:
integration = Integration.objects.get(
external_id=installation_id, status=ObjectStatus.ACTIVE
)
except Integration.DoesNotExist:
return error(request, self.active_organization)
sender_login = (integration.metadata or {}).get("sender", {}).get("login")
# Check that the authenticated GitHub user is the same as who installed the app.
if (
pipeline.fetch_state("github_authenticated_user")
!= sender_login
):
return error(request, self.active_organization)
🤖 Prompt for AI Agents
In `@src/sentry/integrations/github/integration.py` around lines 493 - 505, The
code assumes integration.metadata["sender"]["login"] always exists and can raise
KeyError; update the check around
pipeline.fetch_state("github_authenticated_user") to safely access
integration.metadata by verifying "sender" exists and has a "login" (e.g., check
"sender" in integration.metadata and "login" in integration.metadata["sender"])
or use a helper/get with default None, then only compare when present; reference
the Integration lookup (Integration.objects.get), the integration.metadata
access, and pipeline.fetch_state("github_authenticated_user") and fall back to
returning error(request, self.active_organization) if sender/login is missing or
does not match.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant